/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use anyhow::Result;
use camino::Utf8PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::fmt;
// TODO: remove blanket import
use uniffi_bindgen::bindings::*;
use uniffi_bindgen::pipeline::initial;
use uniffi_pipeline::PrintOptions;

/// TargetLanguage uniffi_bindgen, with a `clap::ValueEnum` derive.
#[derive(Copy, Clone, ValueEnum)]
enum TargetLanguageArg {
    Kotlin,
    Swift,
    Python,
    Ruby,
}

impl fmt::Display for TargetLanguageArg {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Kotlin => write!(f, "kotlin"),
            Self::Swift => write!(f, "swift"),
            Self::Python => write!(f, "python"),
            Self::Ruby => write!(f, "ruby"),
        }
    }
}

impl From<TargetLanguageArg> for TargetLanguage {
    fn from(l: TargetLanguageArg) -> Self {
        match l {
            TargetLanguageArg::Kotlin => Self::Kotlin,
            TargetLanguageArg::Swift => Self::Swift,
            TargetLanguageArg::Python => Self::Python,
            TargetLanguageArg::Ruby => Self::Ruby,
        }
    }
}

// Structs to help our cmdline parsing. Note that docstrings below form part
// of the "help" output.

/// Scaffolding and bindings generator for Rust
#[derive(Parser)]
#[clap(name = "uniffi-bindgen")]
#[clap(version = clap::crate_version!())]
#[clap(propagate_version = true)]
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Generate foreign language bindings
    Generate {
        /// Foreign language(s) for which to build bindings.
        #[clap(long, short, value_enum)]
        language: Vec<TargetLanguageArg>,

        /// Directory in which to write generated files. Default is same folder as .udl file.
        #[clap(long, short)]
        out_dir: Option<Utf8PathBuf>,

        /// Do not try to format the generated bindings.
        #[clap(long, short)]
        no_format: bool,

        /// Path to optional uniffi config file. This config is merged with the `uniffi.toml` config present in each crate, with its values taking precedence.
        #[clap(long, short)]
        config: Option<Utf8PathBuf>,

        /// Deprecated
        ///
        /// This used to signal that a source file is a library rather than a UDL file.
        /// Nowadays, UniFFI will auto-detect this.
        #[clap(long = "library")]
        _library_mode: bool,

        /// When `--library` is passed, only generate bindings for one crate.
        /// When `--library` is not passed, use this as the crate name instead of attempting to
        /// locate and parse Cargo.toml.
        #[clap(long = "crate")]
        crate_name: Option<String>,

        /// Path to the UDL file, or cdylib if `library-mode` is specified
        source: Utf8PathBuf,

        /// Whether we should exclude dependencies when running "cargo metadata".
        /// This will mean external types may not be resolved if they are implemented in crates
        /// outside of this workspace.
        /// This can be used in environments when all types are in the namespace and fetching
        /// all sub-dependencies causes obscure platform specific problems.
        #[clap(long)]
        metadata_no_deps: bool,
    },

    /// Generate Rust scaffolding code
    Scaffolding {
        /// Directory in which to write generated files. Default is same folder as .udl file.
        #[clap(long, short)]
        out_dir: Option<Utf8PathBuf>,

        /// Do not try to format the generated bindings.
        #[clap(long, short)]
        no_format: bool,

        /// Path to the UDL file.
        udl_file: Utf8PathBuf,
    },

    /// Inspect the bindings render pipeline
    Pipeline(PipelineArgs),
}

#[derive(Args)]
struct PipelineArgs {
    /// Pass in a cdylib path rather than a UDL file
    #[clap(long = "library")]
    library_mode: bool,

    /// Path to the UDL file, or cdylib if `library-mode` is specified
    source: Utf8PathBuf,

    /// When `--library` is passed, only generate bindings for one crate.
    /// When `--library` is not passed, use this as the crate name instead of attempting to
    /// locate and parse Cargo.toml.
    #[clap(long = "crate")]
    crate_name: Option<String>,

    /// Whether we should exclude dependencies when running "cargo metadata".
    /// This will mean external types may not be resolved if they are implemented in crates
    /// outside of this workspace.
    /// This can be used in environments when all types are in the namespace and fetching
    /// all sub-dependencies causes obscure platform specific problems.
    #[clap(long)]
    metadata_no_deps: bool,

    /// Bindings Language
    language: TargetLanguageArg,

    /// Only show passes that match <PASS>
    ///
    /// Use `last` to only show the last pass, this can be useful when you're writing new pipelines
    #[clap(short, long)]
    pass: Option<String>,

    /// Don't show diffs for middle passes
    #[clap(long)]
    no_diff: bool,

    /// Only show data for types with name <FILTER_TYPE>
    #[clap(short = 't', long = "type")]
    filter_type: Option<String>,

    /// Only show data for items with fields that match <FILTER>
    #[clap(short = 'n', long = "name")]
    filter_name: Option<String>,
}

fn config_supplier(
    metadata_no_deps: bool,
) -> Result<impl uniffi_bindgen::BindgenCrateConfigSupplier> {
    #[cfg(feature = "cargo-metadata")]
    return uniffi_bindgen::cargo_metadata::CrateConfigSupplier::from_cargo_metadata_command(
        metadata_no_deps,
    );

    #[cfg(not(feature = "cargo-metadata"))]
    Ok(uniffi_bindgen::EmptyCrateConfigSupplier)
}

pub fn run_main() -> anyhow::Result<()> {
    let cli = Cli::parse();
    match cli.command {
        Commands::Generate {
            language,
            out_dir,
            no_format,
            config,
            source,
            crate_name,
            metadata_no_deps,
            ..
        } => {
            if language.is_empty() {
                panic!("please specify at least one language with --language")
            }

            generate(GenerateOptions {
                languages: language.into_iter().map(TargetLanguage::from).collect(),
                out_dir: out_dir
                    .expect("--out-dir is required when generating {language} bindings"),
                source,
                config_override: config,
                crate_filter: crate_name,
                metadata_no_deps,
                format: !no_format,
            })?;
        }
        Commands::Scaffolding {
            out_dir,
            no_format,
            udl_file,
        } => {
            uniffi_bindgen::generate_component_scaffolding(
                &udl_file,
                out_dir.as_deref(),
                !no_format,
            )?;
        }
        Commands::Pipeline(args) => {
            let config_supplier = config_supplier(args.metadata_no_deps)?;
            let initial_root = if args.library_mode {
                initial::Root::from_library(config_supplier, &args.source, args.crate_name)?
            } else {
                initial::Root::from_udl(config_supplier, &args.source, args.crate_name)?
            };

            let opts = PrintOptions {
                pass: args.pass,
                no_diff: args.no_diff,
                filter_type: args.filter_type,
                filter_name: args.filter_name,
            };
            match args.language {
                TargetLanguageArg::Python => python::pipeline().print_passes(initial_root, opts)?,
                language => unimplemented!("{language} does not use the bindings IR pipeline yet"),
            };
        }
    };
    Ok(())
}
